Android避坑指南,发现了一个极度不安全的操作
▲ 点击上方第二个“JsonChao”关注公众号
来一起看一段代码:
public class Student {
private Student() {
throw new IllegalArgumentException("can not create.");
}
public String name;
}
我们如何通过Java代码创建一个Student对象?
我们先想下通过Java创建对象大概有哪些方式:
new Student() // 私有
反射调用构造方法 //throw ex
反序列化 // 需要实现相关序列化接口
clone // 需要实现clone相关接口
...
好了,已经超出我的知识点范畴了。
不免心中嘀咕:
这题目太偏了,毫无意义,而且文章标题是 Android 避坑指南,看起来毫无关系
是的,确实很偏,跳过这个问题,我们往下看,看看是怎么在Android开发过程中遇到的,而且看完后,这个问题就迎刃而解了。
上周一个群有个小伙伴,遇到了一个Kotlin写的Bean,在做Gson将字符串转化成具体的Bean对象时,发生了一个不符合预期的问题。
因为是他们项目的代码,我就不贴了,我写了个类似的小例子来替代。
对于Java Bean,kotlin可以用data class,网上也有很多博客表示:
在 Kotlin 中,不需要自己动手去写一个 JavaBean,可以直接使用 DataClass,使用 DataClass 编译器会默默地帮我们生成一些函数。
我们先写个Bean:
data class Person(var name: String, var age: Int) {
}
这个Bean是用于接收服务器数据,通过Gson转化为对象的。
简化一下代码为:
val gson = Gson()
val person = gson.fromJson<Person>("{\"age\":\"12\"}", Person::class.java)
我们传递了一个json字符串,但是没有包含key为name的值,并且注意:
在Person中name的类型是String,也就是说是不允许name=null的
那么上面的代码,我运行起来结果是什么呢?
报错,毕竟没有传name的值;
不报错,name 默认值为"";
不报错,name=null;
感觉1最合理,也符合Kotlin的空安全检查。
验证一下,修改一下代码,看一下输出:
val gson = Gson()
val person = gson.fromJson<Person>("{\"age\":\"12\"}", Person::class.java)
println(person.name )
输出结果:
null
是不是有些奇怪,感觉意外绕过了Kotlin的空类型检查。
所以那位出问题的同学,在这里之后数据就出了问题,导致一直排查困难。
我们再改一下代码:
data class Person(var name: String, var age: Int): People(){
}
我们让Person继承自People类:
public class People {
public People(){
System.out.println("people cons");
}
}
在People类的构造方法中打印日志。
我们都清楚,正常情况下,一般构造子类对象,必然会先执行父类的构造方法。
运行一下:
没有执行父类构造方法,但对象构造出来了
这里可以猜到,Person对象的构建,并不是常规的构建对象,没有走构造方法。
那么它是怎么做到的呢?
只能去Gson的源码中去找答案了。
找到其怎么做的,其实就相当于解答了我们文首的问题。
Gson这样构造出一个对象,但是没有走父类构造这种,如果真是的这样,那么是极其危险的。
会让程序完全不符合运行预期,少了一些必要逻辑。
所以我们提前说一下,大家不用太惊慌,并不是Gson很容易出现这样的情况,而是恰好上例的写法碰上了,我们一会会说清楚。
首先我们把Person这个kotlin的类,转成Java,避免背后藏了一些东西:
# 反编译之后的显示
public final class Person extends People {
@NotNull
private String name;
private int age;
@NotNull
public final String getName() {
return this.name;
}
public final void setName(@NotNull String var1) {
Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
this.name = var1;
}
public final int getAge() {
return this.age;
}
public final void setAge(int var1) {
this.age = var1;
}
public Person(@NotNull String name, int age) {
Intrinsics.checkParameterIsNotNull(name, "name");
super();
this.name = name;
this.age = age;
}
// 省略了一些方法。
}
可以看到Person有一个包含两参的构造方法,并且这个构造方法中有name的空安全检查。
也就是说,正常通过这个构造方法构建一个Person对象,是不会出现空安全问题的。
那么只能去看看Gson的源码了:
Gson的逻辑,一般都是根据读取到的类型,然后找对应的TypeAdapter去处理,本例为Person对象,所以会最终走到`ReflectiveTypeAdapterFactory.create`然后返回一个TypeAdapter。
我们看一眼其内部代码:
# ReflectiveTypeAdapterFactory.create
@Override
public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
Class<? super T> raw = type.getRawType();
if (!Object.class.isAssignableFrom(raw)) {
return null; // it's a primitive!
}
ObjectConstructor<T> constructor = constructorConstructor.get(type);
return new Adapter<T>(constructor, getBoundFields(gson, type, raw));
}
重点看constructor这个对象的赋值,它一眼就知道跟构造对象相关。
# ConstructorConstructor.get
public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) {
final Type type = typeToken.getType();
final Class<? super T> rawType = typeToken.getRawType();
// ...省略一些缓存容器相关代码
ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType);
if (defaultConstructor != null) {
return defaultConstructor;
}
ObjectConstructor<T> defaultImplementation = newDefaultImplementationConstructor(type, rawType);
if (defaultImplementation != null) {
return defaultImplementation;
}
// finally try unsafe
return newUnsafeAllocator(type, rawType);
}
可以看到该方法的返回值有3个流程:
newDefaultConstructor
newDefaultImplementationConstructor
newUnsafeAllocator
我们先看第一个newDefaultConstructor
private <T> ObjectConstructor<T> newDefaultConstructor(Class<? super T> rawType) {
try {
final Constructor<? super T> constructor = rawType.getDeclaredConstructor();
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
return new ObjectConstructor<T>() {
@SuppressWarnings("unchecked") // T is the same raw type as is requested
@Override public T construct() {
Object[] args = null;
return (T) constructor.newInstance(args);
// 省略了一些异常处理
};
} catch (NoSuchMethodException e) {
return null;
}
}
可以看到,很简单,尝试获取了无参的构造函数,如果能够找到,则通过newInstance反射的方式构建对象。
追随到我们的Person的代码,其实该类中只有一个两参的构造函数,并没有无参构造,从而会命中NoSuchMethodException,返回null。
返回null会走newDefaultImplementationConstructor,这个方法里面都是一些集合类相关对象的逻辑,直接跳过。
那么,最后只能走:newUnsafeAllocator 方法了。
从命名上面就能看出来,这是个不安全的操作。
newUnsafeAllocator最终是怎么不安全的构建出一个对象呢?
往下看,最终执行的是:
public static UnsafeAllocator create() {
// try JVM
// public class Unsafe {
// public Object allocateInstance(Class<?> type);
// }
try {
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
Field f = unsafeClass.getDeclaredField("theUnsafe");
f.setAccessible(true);
final Object unsafe = f.get(null);
final Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class);
return new UnsafeAllocator() {
@Override
@SuppressWarnings("unchecked")
public <T> T newInstance(Class<T> c) throws Exception {
assertInstantiable(c);
return (T) allocateInstance.invoke(unsafe, c);
}
};
} catch (Exception ignored) {
}
// try dalvikvm, post-gingerbread use ObjectStreamClass
// try dalvikvm, pre-gingerbread , ObjectInputStream
}
可以看到Gson在没有找到无参的构造方法后,通过sun.misc.Unsafe构造了一个对象。
注意:Unsafe该类并不是所有的Android 版本中都包含,不过目前新版本都包含,所以Gson这个方法中有3段逻辑都是用来生成对象的,你可以认为3重保险,针对不同平台。本文测试设备:Android 29模拟器
我们这里暂时只讨论sun.misc.Unsafe,其他的其实一个意思。
`sun.misc.Unsafe`何许API?
Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重。
https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html
具体可以参考美团的这篇文章。
好了,到这里就真相大白了。
原因是我们Person没有提供默认的构造方法,Gson在没有找到默认构造方法时,它就直接通过Unsafe的方法,绕过了构造方法,直接构建了一个对象。
到这里,我们收获了:
Gson是如何构建对象的?
我们在写需要Gson转化为对象的类的时候,一定要记得有默认的构造方法,否则虽然不报错,但是很不安全!
我们了解到了还有这种Unsafe黑科技的方式构造对象。
Java中咋么构造一个下面的Student对象呢?
public class Student {
private Student() {
throw new IllegalArgumentException("can not create.");
}
public String name;
}
我们模仿Gson的代码,编写如下:
try {
val unsafeClass = Class.forName("sun.misc.Unsafe")
val f = unsafeClass.getDeclaredField("theUnsafe")
f.isAccessible = true
val unsafe = f.get(null)
val allocateInstance = unsafeClass.getMethod("allocateInstance", Class::class.java)
val student = allocateInstance.invoke(unsafe, Student::class.java)
(student as Student).apply {
name = "zhy"
}
println(student.name)
} catch (ignored: Exception) {
ignored.printStackTrace()
}
输出:
zhy
成功构建。
看到这里,大家可能最大的收获就是了解Gson构建对象流程,以及以后写Bean的时候会注意提供默认的无参构造方法,尤其在使用Kotlin `data class `的时候。
那么刚才我们所说的Unsafe方法在Android上就没有其他实际用处吗?
这个类,提供了类似C语言指针一样操作内存空间的能力。
大家都知道在Android P上面,Google限制了app对hidden API的访问。
但是,Google不能限制自己对hidden API访问对吧,所以它自己的相关类,是允许访问hidden API的。
那么Google是如何区分是我们app调用,还是它自己调用呢?
其中有一个办法就是通过ClassLoader,系统认为如果ClassLoader为BootStrapClassLoader则就认为是系统类,则放行。
那么,我们突破P访问限制,其中一个思路就是,搞一个类,把它的ClassLoader换成BootStrapClassLoader,从而可以反射任何hidden api。
怎么换呢?
只要把这个类的classLoader成员变量设置为null就可以了。
参考代码:
private void testJavaPojie() {
try {
Class reflectionHelperClz = Class.forName("com.example.support_p.ReflectionHelper");
Class classClz = Class.class;
Field classLoaderField = classClz.getDeclaredField("classLoader");
classLoaderField.setAccessible(true);
classLoaderField.set(reflectionHelperClz, null);
} catch (Exception e) {
e.printStackTrace();
}
}
来自:https://juejin.im/post/5ba0f3f7e51d450e6f2e39e0
但是这样有个问题,上面的代码用到了反射修改一个类的classLoader成员,假设google有一天把反射设置classLoader也完全限制掉,就不行了。
那么怎么办?原理还是换ClassLoader,但是我们不走Java反射的方式了,而是用Unsafe:
参考代码:
@Keep
public class ReflectWrapper {
//just for finding the java.lang.Class classLoader field's offset
@Keep
private Object classLoaderOffsetHelper;
static {
try {
Class<?> VersionClass = Class.forName("android.os.Build$VERSION");
Field sdkIntField = VersionClass.getDeclaredField("SDK_INT");
sdkIntField.setAccessible(true);
int sdkInt = sdkIntField.getInt(null);
if (sdkInt >= 28) {
Field classLoader = ReflectWrapper.class.getDeclaredField("classLoaderOffsetHelper");
long classLoaderOffset = UnSafeWrapper.getUnSafe().objectFieldOffset(classLoader);
if (UnSafeWrapper.getUnSafe().getObject(ReflectWrapper.class, classLoaderOffset) instanceof ClassLoader) {
Object originalClassLoader = UnSafeWrapper.getUnSafe().getAndSetObject(ReflectWrapper.class, classLoaderOffset, null);
} else {
throw new RuntimeException("not support");
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
来自作者区长:一种纯 Java 层绕过 Android P 私有函数调用限制的方式,一文。
Unsafe赋予了我们操作内存的能力,也就能完成一些平时只能依赖C++完成的代码。
好了,从一位朋友遇到的问题,由此引发了一整篇文章的讨论,希望你能有所收获。
END
往期推荐
欢迎关注我的微信:bcce5360,群人数已超200,无法扫码入群,加我微信拉你进群。
点击下方卡片关注JsonChao,为你构建一套
未来Android开发必备的知识体系
▲ 点击上方卡片关注 JsonChao,构建一套
未来 Android 开发必备的知识体系
欢迎把文章分享到朋友圈
很感谢您阅读这篇文章,希望您能将它分享给您的朋友或技术群,这对我意义重大。
你若喜欢,为JsonChao点个在看哦